/**
 * cdn-uploader.js
 * 用于将构建好的微前端应用部署到各种云存储CDN
 * 支持腾讯云COS、阿里云OSS、七牛云
 */
const fs = require('fs').promises;
const path = require('path');
const { createLogger } = require('./logger');
const { promisify } = require('util');
const glob = promisify(require('glob'));
const mime = require('mime-types');

// 创建日志记录器
const logger = createLogger('cdn-uploader');

// 加载云存储SDK
let COS, OSS, qiniu;

/**
 * 初始化SDK
 */
async function initSDKs() {
  try {
    // 按需加载SDK
    COS = require('cos-nodejs-sdk-v5');
    logger.info('腾讯云COS SDK已加载');
  } catch (err) {
    logger.warn('腾讯云COS SDK加载失败，COS上传功能将不可用');
  }

  try {
    // 按需加载SDK
    OSS = require('ali-oss');
    logger.info('阿里云OSS SDK已加载');
  } catch (err) {
    logger.warn('阿里云OSS SDK加载失败，OSS上传功能将不可用');
  }

  try {
    // 按需加载SDK
    qiniu = require('qiniu');
    logger.info('七牛云SDK已加载');
  } catch (err) {
    logger.warn('七牛云SDK加载失败，七牛云上传功能将不可用');
  }
}

/**
 * 获取所有待上传的文件列表
 * @param {string} distDir 构建目录路径
 * @returns {Promise<Array>} 文件列表
 */
async function getFileList(distDir) {
  try {
    // 检查目录是否存在
    await fs.access(distDir);
    
    // 获取所有文件
    const files = await glob('**/*', { cwd: distDir, nodir: true });
    
    // 按路径排序，让index.html靠前以便于调试
    return files.sort((a, b) => {
      if (a.includes('index.html') && !b.includes('index.html')) return -1;
      if (!a.includes('index.html') && b.includes('index.html')) return 1;
      return a.localeCompare(b);
    });
  } catch (err) {
    logger.error(`获取文件列表失败: ${err.message}`);
    throw err;
  }
}

/**
 * 获取文件的MIME类型
 * @param {string} filePath 文件路径
 * @returns {string} MIME类型
 */
function getMimeType(filePath) {
  return mime.lookup(filePath) || 'application/octet-stream';
}

/**
 * 获取缓存控制策略
 * @param {string} filePath 文件路径
 * @param {string} cacheControl 缓存策略
 * @returns {string} 缓存控制头
 */
function getCacheControl(filePath, cacheControl = 'default') {
  // HTML文件通常不应该被缓存，以便于部署更新
  const isHtml = filePath.endsWith('.html');
  
  switch (cacheControl) {
    case 'no-cache':
      return 'no-cache, no-store, must-revalidate';
    case 'short':
      return isHtml ? 'no-cache' : 'max-age=3600';
    case 'long':
      return isHtml ? 'no-cache' : 'max-age=31536000';
    case 'default':
    default:
      return isHtml ? 'no-cache, no-store, must-revalidate' : 'max-age=31536000';
  }
}

/**
 * 创建腾讯云COS上传器
 * @param {Object} config 存储配置
 * @returns {Object} 上传器
 */
function createCOSUploader(config) {
  if (!COS) {
    throw new Error('腾讯云COS SDK未加载');
  }
  
  const cos = new COS({
    SecretId: config.secretId,
    SecretKey: config.secretKey
  });
  
  return {
    upload: async (localFile, distDir, basePath, remoteFile, options = {}) => {
      const key = path.join(basePath, remoteFile).replace(/\\/g, '/');
      const fullLocalPath = path.join(distDir, remoteFile);
      
      const uploadParams = {
        Bucket: config.bucket,
        Region: config.region,
        Key: key,
        Body: await fs.readFile(fullLocalPath),
        ContentType: getMimeType(fullLocalPath),
        CacheControl: getCacheControl(fullLocalPath, options.cacheControl)
      };
      
      try {
        const result = await new Promise((resolve, reject) => {
          cos.putObject(uploadParams, (err, data) => {
            if (err) reject(err);
            else resolve(data);
          });
        });
        
        return {
          success: true,
          url: `https://${config.bucket}.cos.${config.region}.myqcloud.com/${key}`,
          cdnUrl: config.cdnDomain ? `${config.cdnDomain}/${key}` : null,
          size: (await fs.stat(fullLocalPath)).size
        };
      } catch (err) {
        logger.error(`COS上传失败 ${key}: ${err.message}`);
        throw err;
      }
    }
  };
}

/**
 * 创建阿里云OSS上传器
 * @param {Object} config 存储配置
 * @returns {Object} 上传器
 */
function createOSSUploader(config) {
  if (!OSS) {
    throw new Error('阿里云OSS SDK未加载');
  }
  
  const oss = new OSS({
    accessKeyId: config.secretId,
    accessKeySecret: config.secretKey,
    bucket: config.bucket,
    region: config.region
  });
  
  return {
    upload: async (localFile, distDir, basePath, remoteFile, options = {}) => {
      const key = path.join(basePath, remoteFile).replace(/\\/g, '/');
      const fullLocalPath = path.join(distDir, remoteFile);
      
      try {
        const headers = {
          'Content-Type': getMimeType(fullLocalPath),
          'Cache-Control': getCacheControl(fullLocalPath, options.cacheControl)
        };
        
        const result = await oss.put(key, await fs.readFile(fullLocalPath), {
          headers
        });
        
        return {
          success: true,
          url: result.url,
          cdnUrl: config.cdnDomain ? `${config.cdnDomain}/${key}` : null,
          size: (await fs.stat(fullLocalPath)).size
        };
      } catch (err) {
        logger.error(`OSS上传失败 ${key}: ${err.message}`);
        throw err;
      }
    }
  };
}

/**
 * 创建七牛云上传器
 * @param {Object} config 存储配置
 * @returns {Object} 上传器
 */
function createQiniuUploader(config) {
  if (!qiniu) {
    throw new Error('七牛云SDK未加载');
  }
  
  const mac = new qiniu.auth.digest.Mac(config.secretId, config.secretKey);
  const putPolicy = new qiniu.rs.PutPolicy({ scope: config.bucket });
  const uploadToken = putPolicy.uploadToken(mac);
  
  const qiniuConfig = new qiniu.conf.Config();
  qiniuConfig.zone = qiniu.zone[config.region] || qiniu.zone.Zone_z0;
  const formUploader = new qiniu.form_up.FormUploader(qiniuConfig);
  const putExtra = new qiniu.form_up.PutExtra();
  
  return {
    upload: async (localFile, distDir, basePath, remoteFile, options = {}) => {
      const key = path.join(basePath, remoteFile).replace(/\\/g, '/');
      const fullLocalPath = path.join(distDir, remoteFile);
      
      try {
        const result = await new Promise((resolve, reject) => {
          formUploader.putFile(uploadToken, key, fullLocalPath, putExtra, (err, body, info) => {
            if (err) reject(err);
            else if (info.statusCode === 200) resolve(body);
            else reject(new Error(`七牛云上传失败: ${info.statusCode}`));
          });
        });
        
        return {
          success: true,
          url: config.cdnDomain ? `${config.cdnDomain}/${key}` : null,
          cdnUrl: config.cdnDomain ? `${config.cdnDomain}/${key}` : null,
          size: (await fs.stat(fullLocalPath)).size
        };
      } catch (err) {
        logger.error(`七牛云上传失败 ${key}: ${err.message}`);
        throw err;
      }
    }
  };
}

/**
 * 创建上传器
 * @param {Object} config 存储配置
 * @returns {Object} 上传器
 */
function createUploader(config) {
  switch (config.provider.toLowerCase()) {
    case 'cos':
    case 'tencent':
    case 'tencentcloud':
      return createCOSUploader(config);
    case 'oss':
    case 'aliyun':
    case 'alibaba':
      return createOSSUploader(config);
    case 'qiniu':
    case 'qiniucloud':
      return createQiniuUploader(config);
    default:
      throw new Error(`不支持的存储提供商: ${config.provider}`);
  }
}

/**
 * 构建部署路径（支持灰度发布）
 * @param {string} basePath 基础路径
 * @param {string} appName 应用名称
 * @param {string} deployType 部署类型：'prod' 或 'gray'
 * @param {string} version 版本号（仅灰度部署需要）
 * @returns {string} 最终部署路径
 */
function buildDeployPath(basePath, appName, deployType = 'prod', version = null) {
  // 根据灰度发布方案：
  // 生产版本：{basePath}/{appName}/
  // 灰度版本：{basePath}/{appName}/{version}/
  
  if (deployType === 'gray') {
    if (!version) {
      throw new Error('灰度部署必须提供版本号');
    }
    // 灰度版本路径：www/micro-frontend/app1/v1.2.0/
    return path.join(basePath, appName, version).replace(/\\/g, '/');
  } else {
    // 生产版本路径：www/micro-frontend/app1/
    return path.join(basePath, appName).replace(/\\/g, '/');
  }
}

/**
 * 部署应用到CDN
 * @param {string} appName 应用名称
 * @param {Object} config 存储配置
 * @param {Object} options 部署选项
 * @param {string} options.deployType 部署类型：'prod'（生产）或 'gray'（灰度），默认 'prod'
 * @param {string} options.version 版本号（灰度部署时必填，如 'v1.2.0'）
 * @param {string} options.basePath 基础路径，默认 'www/micro-frontend'
 * @param {string} options.cacheControl 缓存控制策略
 * @param {boolean} options.parallel 是否并行上传
 * @param {boolean} options.overwrite 是否覆盖已存在文件
 * @param {boolean} options.cleanOld 是否清理旧文件
 * @param {number} options.batchSize 并行上传的文件数
 * @param {Function} options.progressCallback 进度回调函数
 * @returns {Promise<Object>} 部署结果
 */
async function deployToCDN(appName, config, options = {}) {
  // 确保SDK已初始化
  await initSDKs();
  
  // 默认选项
  const defaultOptions = {
    basePath: 'www/micro-frontend',
    deployType: 'prod', // 默认生产部署
    version: null, // 灰度版本号
    cacheControl: 'default',
    parallel: true,
    overwrite: true,
    cleanOld: false,
    batchSize: 10, // 并行上传的文件数
    progressCallback: null, // 进度回调函数
  };
  
  const deployOptions = { ...defaultOptions, ...options };
  
  // 验证灰度部署参数
  if (deployOptions.deployType === 'gray' && !deployOptions.version) {
    throw new Error('灰度部署必须提供版本号（options.version）');
  }
  
  // 构建部署路径
  const finalBasePath = buildDeployPath(
    deployOptions.basePath,
    appName,
    deployOptions.deployType,
    deployOptions.version
  );
  
  const deployTypeText = deployOptions.deployType === 'gray' 
    ? `灰度版本 ${deployOptions.version}` 
    : '生产版本';
  
  logger.info(`开始部署应用 ${appName} (${deployTypeText}) 到 ${config.provider} 存储，路径: ${finalBasePath}`);
  
  try {
    // 创建对应的上传器
    const uploader = createUploader(config);
    
    // 确定本地dist目录
    const rootDir = path.resolve(__dirname, '../../../');
    const distDir = path.join(rootDir, 'apps', appName, 'dist');
    
    // 获取待上传文件列表
    const files = await getFileList(distDir);
    const totalFiles = files.length;
    
    if (totalFiles === 0) {
      throw new Error(`没有找到可上传的文件，请确保应用 ${appName} 已构建`);
    }
    
    logger.info(`找到 ${totalFiles} 个文件需要上传`);
    
    // 计算每个文件的基本信息
    const fileDetails = await Promise.all(files.map(async (file) => {
      const stats = await fs.stat(path.join(distDir, file));
      return {
        path: file,
        size: stats.size,
        uploaded: false,
        error: null
      };
    }));
    
    // 上传进度
    let uploadedFiles = 0;
    let totalSize = fileDetails.reduce((acc, file) => acc + file.size, 0);
    let uploadedSize = 0;
    let errors = [];
    
    // 更新进度
    const updateProgress = (file, success, error = null) => {
      if (success) {
        uploadedFiles++;
        uploadedSize += file.size;
      }
      
      if (error) {
        errors.push({ file: file.path, error });
      }
      
      const progress = {
        appName,
        totalFiles,
        uploadedFiles,
        totalSize,
        uploadedSize,
        percentage: Math.floor((uploadedSize / totalSize) * 100),
        errors
      };
      
      if (deployOptions.progressCallback) {
        deployOptions.progressCallback(progress);
      }
      
      return progress;
    };
    
    // 根据是否并行决定上传方式
    if (deployOptions.parallel) {
      // 批量并行上传
      const batchSize = deployOptions.batchSize;
      for (let i = 0; i < fileDetails.length; i += batchSize) {
        const batch = fileDetails.slice(i, i + batchSize);
        
        await Promise.all(batch.map(async (file) => {
          try {
            await uploader.upload(
              file.path,
              distDir,
              finalBasePath,
              file.path,
              { cacheControl: deployOptions.cacheControl }
            );
            updateProgress(file, true);
          } catch (err) {
            updateProgress(file, false, err.message);
          }
        }));
      }
    } else {
      // 串行上传
      for (const file of fileDetails) {
        try {
          await uploader.upload(
            file.path,
            distDir,
            finalBasePath,
            file.path,
            { cacheControl: deployOptions.cacheControl }
          );
          updateProgress(file, true);
        } catch (err) {
          updateProgress(file, false, err.message);
        }
      }
    }
    
    // 部署结果
    const success = errors.length === 0;
    const indexHtmlPath = path.join(finalBasePath, 'index.html').replace(/\\/g, '/');
    const result = {
      appName,
      storageProvider: config.provider,
      bucket: config.bucket,
      basePath: finalBasePath,
      deployType: deployOptions.deployType,
      version: deployOptions.version,
      totalFiles,
      uploadedFiles,
      errors,
      success,
      cdnUrl: config.cdnDomain ? `${config.cdnDomain}/${finalBasePath}/` : null,
      indexHtmlUrl: config.cdnDomain ? `${config.cdnDomain}/${indexHtmlPath}` : null,
      timestamp: new Date().toISOString()
    };
    
    if (success) {
      logger.info(`应用 ${appName} (${deployTypeText}) 部署成功! 上传了 ${uploadedFiles} 个文件`);
      logger.info(`部署路径: ${finalBasePath}`);
      if (result.indexHtmlUrl) {
        logger.info(`入口文件URL: ${result.indexHtmlUrl}`);
      }
    } else {
      logger.warn(`应用 ${appName} (${deployTypeText}) 部署完成，但有 ${errors.length} 个文件上传失败`);
    }
    
    return result;
  } catch (err) {
    logger.error(`部署应用 ${appName} 失败: ${err.message}`);
    throw err;
  }
}

module.exports = {
  initSDKs,
  deployToCDN
}; 